package ads.server;

import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;

import org.apache.log4j.Logger;

import spread.SpreadConnection;
import spread.SpreadException;
import spread.SpreadGroup;
import spread.SpreadMessage;
import ads.exception.InvalideInvokerException;
import ads.exception.ServerSpreadException;
import ads.exception.SpreadRemoteException;
import ads.util.GameInfo;
import ads.util.PlayerInfo;
import ads.util.ServerConfig;

public abstract class SpreadMember implements SpreadMemberRemoteInterface {
	private static final Logger log = Logger.getLogger(SpreadMember.class
			.getName());
	protected SpreadConnection connection = null;
	protected SpreadGroup group = null;
	protected ServerConfig config;
	protected ServerFifoMessageFactory msgFactory;
	private ServerBasicMessageListener serverBasicMessageListener;
	private ServerAdvanceMessageListener serverAdvanceMessageListener;
	/**
	 * the identification of this server instance
	 */
	protected SpreadGroup privateServerGroup = null;
	protected String primaryServerName = null;
	// protected ArrayList<String> backupServers = new ArrayList<String>();

	protected ArrayList<PlayerInfo> players = new ArrayList<PlayerInfo>();
	protected HashMap<Integer, GameInfo> games = new HashMap<Integer, GameInfo>();
	protected SpreadRequestor requestor = null;
	protected ServerMain gui;

	public synchronized Boolean IsPrimaryServer() {
		if (primaryServerName != null
				&& privateServerGroup.toString().equals(primaryServerName))
			return true;
		else
			return false;

	}

	public synchronized void setPrimaryServer(String primaryServer) {
		this.primaryServerName = primaryServer;
	}

	protected SpreadMember(ServerConfig config, ServerMain gui)
			throws ServerSpreadException {
		super();
		this.config = config;
		this.gui = gui;
		log.debug("daemonHost:" + config.getDaemonHost());
		log.debug("daemonPort:" + config.getDaemonPort());
		log.debug("serverName:" + config.getPrivateName());
		log.debug("groupName:" + config.getGroupName());
		initSpread();
	}

	private void initSpread() throws ServerSpreadException {
		try {
			log.debug("initialize spread...");
			log.info("connecting to spread daemon:" + config.getDaemonHost()
					+ ":" + config.getDaemonPort());
			connection = new SpreadConnection();
			connection.connect(InetAddress.getByName(config.getDaemonHost()),
					config.getDaemonPort(), config.getPrivateName(), false,
					true);
			group = new SpreadGroup();
			log.info("join to group:" + config.getGroupName());
			group.join(connection, config.getGroupName());
			requestor = new SpreadRequestor(this);
			privateServerGroup = connection.getPrivateGroup();
			log.info("identification of this server is " + privateServerGroup);
			serverAdvanceMessageListener = new ServerAdvanceMessageListener(
					this);
			serverBasicMessageListener = new ServerBasicMessageListener(this);
			log.debug("add message listener");
			connection.add(serverAdvanceMessageListener);
			// connection.add(serverBasicMessageListener);
			log.debug("create ServerFifoMessageFactory");
			SpreadMessage msg = new SpreadMessage();
			msg.addGroup(config.getGroupName());
			msg.setFifo();
			msgFactory = new ServerFifoMessageFactory(msg);
			String ps = null;
			try {
				ps = requestor.queryPrimaryServer();

			} catch (SpreadRemoteException e) {
				ps = null;
			}
			if (ps != null)
				setPrimaryServer(ps);
			else
				setPrimaryServer(this.privateServerGroup.toString());
			if (this.IsPrimaryServer()) {
				requestor.updateServers(primaryServerName);
			}
		} catch (SpreadException e) {
			log.error(e);
			throw new ServerSpreadException(
					"SpreadException,Failed to initialize spread connection", e);
		} catch (Exception e) {
			log.error(e);
			throw new ServerSpreadException(e.getClass()
					+ ",Failed to initialize spread connection", e);
		}

	}

	protected void leaveFromGroup() throws SpreadException {
		log.debug("remove message listener");
		// connection.remove(serverBasicMessageListener);
		connection.remove(serverAdvanceMessageListener);
		log.info("leave from group" + config.getGroupName());
		group.leave();
		log.info("disconnect...");
		connection.disconnect();
	}

	/**
	 * update backup server.
	 * 
	 * @param operator
	 * @param obj
	 *            must be a instance of PlayerInfo or GameInfo
	 * @throws SpreadException
	 */
	protected void Update(UpdateOperator operator, Object obj)
			throws SpreadException {
		SpreadMessage msg = msgFactory.createMessage(operator, obj);
		log
				.info("send update message.operator:" + operator + ", object:"
						+ obj);
		connection.multicast(msg);

	}

	@Override
	public String queryPrimaryServer() throws SpreadRemoteException,
			InvalideInvokerException {
		if (IsPrimaryServer())
			return privateServerGroup.toString();
		else
			throw new InvalideInvokerException();
	}

	@Override
	public URI getPrimaryServerURI() throws SpreadRemoteException,
			InvalideInvokerException {
		if (IsPrimaryServer()) {
			try {
				return formatURI();
			} catch (URISyntaxException e) {
				e.printStackTrace();
				throw new SpreadRemoteException(e);
			}
		} else
			throw new InvalideInvokerException();
	}

	protected URI formatURI() throws URISyntaxException {
		URI uri = new URI("rmi://" + config.getRegstryHost() + ":"
				+ config.getRegstryPort() + "/" + config.getServerName());

		return uri;
	}

	@Override
	public void updateServers(String ps) throws SpreadRemoteException {
		log.info("updateServers, the primary server is " + ps);
		synchronized (primaryServerName) {
			this.primaryServerName = ps;
		}

	}

	public void handleMemberLeftOrDisconnected(SpreadGroup downServer,
			SpreadGroup[] members) {
		log.info("handle Member Left Or Disconnected," + downServer
				+ ", members:" + members);
		boolean primaryServerChanged = false;
		synchronized (primaryServerName) {
			if (primaryServerName.equals(downServer.toString())) {
				/* select a new primary server */
				String ps = members[0].toString();
				for (int i = 1; i < members.length - 1; i++) {
					if (ps.compareTo(members[i].toString()) < 0) {
						ps = members[i].toString();
					}
				}
				primaryServerChanged = true;
				primaryServerName = ps;
			}
		}
		if (primaryServerChanged) {
			log.info("primaryServerChanged, this server is primary server");
			gui.setPrimaryServer(primaryServerName);
		}
		if (primaryServerChanged
				&& primaryServerName.equals(privateServerGroup.toString())) {
			try {
				requestor.updateServers(primaryServerName);
			} catch (SpreadRemoteException e) {
				log.error("error by update primary server", e);
			}
			accessionToPrimaryServer();
		}

	}

	public void handleMemberJoined(SpreadGroup server) {
		log.info("handle Member "+server+"Joined" );
		if(this.IsPrimaryServer()){
			for(PlayerInfo p:players){
				try {
					Update(UpdateOperator.addorUpdate, p);
				} catch (SpreadException e) {
					log.error("Error by add or update PlayerInfo",e);
				}
			}
			for(GameInfo g:games.values()){
				try {
					Update(UpdateOperator.addorUpdate, g);
				} catch (SpreadException e) {
					log.error("Error by add or update GameInfo",e);
				}
			}
			
		}
	}

	/**
	 * this method was called when it was to be a primary server
	 */
	public abstract void accessionToPrimaryServer();

	public void updatePlayerInfo(UpdateOperator op, PlayerInfo pi) {
		switch (op) {
		case add:
			synchronized (players) {
				players.add(pi);
			}
			break;
		case delete:
			synchronized (players) {
				if (players.contains(pi)) {
					players.remove(pi);
				}
			}
			break;
		case update:
			synchronized (players) {
				if (players.contains(pi)) {
					players.remove(pi);
					players.add(pi);
				}
			}
			break;
		case addorUpdate:
			synchronized (players) {
				if (players.contains(pi)) {
					players.remove(pi);
				}
				players.add(pi);
			}
			break;
		}
		log.debug(players);
	}

	public void updateGameInfo(UpdateOperator op, GameInfo gi) {
		switch (op) {
		case delete:
			synchronized (games) {
				games.remove(gi.getId());
			}
			break;
		case add:
		case update:
		case addorUpdate:
			synchronized (games) {
				games.put(gi.getId(), gi);
			}
			break;
		}
		log.debug(games);
	}
}
